package com.espirit.eap.sa;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Resource;
import javax.persistence.PersistenceException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.annotations.Synchronize;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.eap.manager.DBAManager;
import com.espirit.eap.pagelayout.DynamicModelService;
import com.espirit.eap.pagelayout.PageUpgradeService;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.Initialization;
import com.googlecode.cswish.struts.hibernate.DbTools;
import com.googlecode.cswish.struts.hibernate.DynamicClassLoaderFactory;
import com.googlecode.cswish.struts.hibernate.MultiSiteDataSource;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.struts.spring.SystemEnv;
import com.googlecode.cswish.util.FileSyncService;
import com.googlecode.cswish.util.FileUtil;
import com.googlecode.cswish.util.IWebsiteService;
import com.googlecode.cswish.util.OgnlExUtil;
import com.googlecode.cswish.util.bean.ModelUtil;

@Service
public class WebsiteService implements IWebsiteService {
	
	private static final Log logger = LogFactory.getLog(WebsiteService.class);

	@Resource
	private GenericService genericService;
	
	@Resource
	private Config config;
	
	@Resource
	private ObjectFactory objectFactory;
	
	@Resource
	private OgnlExUtil ognlExUtil;
	
	@Resource
	private LoginUserService loginUserService;
	
	@Resource
	private SafeManager safeManager;
	
	@Resource
	private CacheService cacheService;
	
	@Resource
	private MultiSiteDataSource multiSiteDataSource;
	
	@Resource
	private DynamicClassLoaderFactory dynamicClassLoaderFactory;
	
	@Resource
	private FileSyncService fileSyncService;
	
	@Resource
	DBAManager dbaManager;
	
	private WebsiteService websiteService;
	
	private int maxSiteCount;
	
	private final static Map<String , Website> websites = new ConcurrentHashMap<String ,Website>();
	
	
//	public void set(DBAManager dbaManager){
//		this.dbaManager = dbaManager;
//	}
	
	private WebsiteService getWebsiteService() {
		if (websiteService == null) {
			websiteService = objectFactory.getBean(WebsiteService.class, false);
		}
		return websiteService;
	}
	
	public String getDefaultWebsiteName() {
		String siteName = System.getProperty("website.defaultName");
		if (StringUtils.isEmpty(siteName)) {
			if (websites.size() > 0) {
				siteName = websites.keySet().iterator().next();
			} else {
				siteName = Website.DEFAULT_NAME;
			}
		}
		return siteName;
	}
	
	public Collection<Website> getWebsites() {
		return websites.values();
	}
	
	@Override
	public Collection<String> getWebsiteNames() {
		return new ArrayList<>(websites.keySet());
	}
	
	public Website updateWebsite(String siteName, String password, String loginPage, String homePage) {
		Website	website = new Website();
		website.setCompId(Integer.parseInt(siteName));
//		website.setName(siteName);
		Website defaultSite = genericService.searchAll(website).getData(0);
		if (defaultSite != null) {
			website = defaultSite;
		} else {
			website.setStatus(Website.STATUS_INIT);
		}
		
		if (loginPage != null) {
			website.setLoginPage(loginPage);
		}
		if (homePage != null) {
			website.setHomePage(homePage);
		}
		if (password != null) {
			password = loginUserService.getEncryptPassword(password);
			website.setPassword(password);
		}
		Date time = Calendar.getInstance().getTime();
		website.setModifyOn(time);
		if (website.getCreatedOn() == null) {
			website.setCreatedOn(time);
		}
		if (website.getTemplateSite() == null) {
			website.setTemplateSite(true);
		}
		website = genericService.merge(website);
		return website;
	}
	
	
	public Website getWebsite(String modelSimpleName){
		if (modelSimpleName == null || modelSimpleName.length() == 0) {
			return null;
		}
		
		int index = modelSimpleName.indexOf(".");
		String site;
		if (index == -1) {
			site = modelSimpleName;
		} else {
			site = modelSimpleName.substring(0, index);
		}
		
		Website website = websites.get(site);
		if (website != null){
			return website;
		} else {
			boolean isEnableSite = config.isEnableSaas() || site.equals(getDefaultWebsiteName());
			if (isEnableSite && (maxSiteCount == 0 || websites.size() <= maxSiteCount)) {
				// NOTICE: set the site manually in order to avoid active the the valid data source
				Website dbWebsite;
				ContextHolder contextHolder = ContextHolder.get();
				String oldSite = contextHolder.getSiteName();
				contextHolder.setSiteName(Website.DEFAULT_NAME);
				try {
					try {
						// get the site definition from default site db
						dbWebsite = genericService.executeInSite(Website.DEFAULT_NAME, new Executor<Website, String>() {
							@Override
							public Website execute(String siteName) {
								return loadWebSite(siteName, false);
							}
						}, site);
					} catch (TransactionSystemException ex) {
						logger.debug("Found exception, is the saas site changed? update the saas site and try it again");
						// get the site definition from default site db
						dbWebsite = genericService.executeInSite(Website.DEFAULT_NAME, new Executor<Website, String>() {
							@Override
							public Website execute(String siteName) {
								// try to init the default site before using it
//								DynamicModelManager dynamicModelManager = ObjectFactory.getInstance()
//										.getBean(DynamicModelManager.class, false);
//								dynamicModelManager.refreshDbSchema();
								return loadWebSite(siteName, false);
							}
						}, site);
					}
				} finally {
					contextHolder.setSiteName(oldSite);
				}
				return dbWebsite;
			} else {
				throw new ApplicationException("errors.maxSiteCount", maxSiteCount);
			}
		}
	}
	
	/**
	 * make sure that this method runs in the 'SAAS' site
	 * 
	 * @param site
	 * @param startup
	 * @return
	 */
	@Transactional
	public Website loadWebSite(String site, boolean startup) {
		if (logger.isDebugEnabled()) {
			logger.debug("load site: " + site);
		}
		
		Website website = null;
		Website example = new Website();
		example.setCompId(Integer.parseInt(site));
//		example.setName(site);
		try {
			website = genericService.searchAll(example).getData(0);
			if (website == null) return null ;
			
			String productName = website.getProductName();
			// check the plug-in folder and build the site automatically
			String siteCfg = config.getPluginDir(productName) + "website.properties";
			File siteCfgFile = new File(siteCfg);
			boolean fileIsNew = false;
			if (website != null && website.getModifyOn() != null && siteCfgFile.exists()) {
				fileIsNew = siteCfgFile.lastModified() > website.getModifyOn().getTime();
			}
			
			if (website == null || fileIsNew) {
				if (siteCfgFile.exists()) {
					try {
						Properties properties = new Properties();
						properties.load(new FileInputStream(siteCfgFile));
						
						if (!Website.STATUS_DISABLE.equals(properties.getProperty("website.status"))) {
							// add the website automatically
							String password = properties.getProperty("website.password");
							String loginPage = properties.getProperty("website.loginPage");
							String homePage = properties.getProperty("website.homePage");
							website = updateWebsite(site, password, loginPage, homePage);
							
							// uninstall the removed plugin
							uninstallWebsitePlugin(site, properties);
						}
					} catch (IOException ex) {
						logger.error("Failed to load site properties", ex);
					}
				}
			}
			
			if (website != null) {
				Website copy = new Website();
				ModelUtil.deepCopy(website, copy, Collections.EMPTY_LIST, false);
				website = copy;
				websites.put(site, website);		// add to cache firstly, avoid the dead cycle
				if (startup) {
					startup(website.getName());
				}
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("Site " + site + " doesn't exist");
				}
			}
		} catch (PersistenceException ex) {
			websites.remove(site);
			if (logger.isDebugEnabled()) {
				logger.debug("Can't find the site information from db:" + site, ex);
			}
		}
		return website;
	}
	
	private void uninstallWebsitePlugin(String site, Properties properties) {
		for (Entry entry : properties.entrySet()) {
			String key = (String) entry.getKey();
			if (key.startsWith("plugins.")) {
				if ("false".equals(entry.getValue())) {
					String plugin = key.substring("plugins.".length());
					objectFactory.getBean(PageUpgradeService.class, false).uninstall(site, plugin);
				}
			}
		}
	}
	
	public void _add(Website model){
		if (this.dbaManager != null && this.dbaManager.getDefaultWebsite() != null){
			Website site = this.dbaManager.getDefaultWebsite();
			if (site == null) return ;
			model.setLoginPage(site.getLoginPage());
			model.setProductName(site.getProductName());
			model.setPassword(site.getPassword());
			model.setUserCount(site.getUserCount());
		}
		model.setCompId(getNextCompId());
	}
	
	private synchronized Integer getNextCompId(){
		String ql = "select max(a.compId) from " + Website.class.getName() + " a";
		Integer maxCompId = (Integer) genericService.searchByQl(new QLInfo(ql, true)).getData(0);
		if (maxCompId == null || maxCompId == Website.DEFAULT_SID){
			return 10001;
		} else {
			return maxCompId + 1;
		}
	}

	public Website add(Website model) {
		checkSiteName(model.getName());
			
		model.setStatus(Website.STATUS_INIT);
		model.setCreatedOn(Calendar.getInstance().getTime());
		if (model.getProductName() == null && this.dbaManager.getDefaultWebsite() != null){
			model.setProductName(this.dbaManager.getDefaultWebsite().getProductName());
		}
		if (model.getPassword() != null) {
			String password = loginUserService.getEncryptPassword(model.getPassword());
			model.setPassword(password);
		}
		Website website = genericService.add(model);
		
		website = website.clone();
		// TODO: use Message Event center + Hibernate interceptor?
		updateCurrentUserPrivilege(model);
		
		String oldSite = ContextHolder.get().getSiteName();
		ContextHolder.get().setSiteName(model.getName());

		this.dbaManager.initSaasData();
		
		getWebsiteService().install(website, true);
		
		
		ContextHolder.get().setSiteName(oldSite);
		
		return website;
	}
	
	private void checkSiteName(String site) {
		boolean disable = "test".equals(site) || "information_schema".equals(site) || "mysql".equals(site);
		if (!disable) {
			for (int i = 0, len = site.length(); i < len; i++) {
				char c = site.charAt(i);
				if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_') {
				} else {
					disable = true;
					break;
				}
			}
		}
		
		if (disable) {
			throw new ApplicationException("errors.invalidSiteName");
		}
	}

	private String getInitSql(String siteName, boolean isEclipseEnv) {
		String dbHome = System.getProperty(SystemEnv.DB_HOME);
		String siteInitSql = dbHome + "backup/" + siteName + "-init.sql";
		if (isEclipseEnv) {
			if (!new File(siteInitSql).exists()) {
				siteInitSql = System.getProperty(SystemEnv.PROJECT_HOME) + "db/" + siteName + "-init.sql";
			}
		}
		return siteInitSql;
	}
	
	/**
	 * Create new database according to the template database
	 * 
	 * @param website
	 */
	@Async
	public synchronized void install(Website website, boolean startupIt) {
		String siteName = website.getName();
		if (logger.isDebugEnabled()) {
			logger.debug("Begin init site: " + siteName);
		}
		
		stop(siteName);
		
		List<Website> webTemplates = website.getWebsiteTemplates();
		boolean hasTemplate = webTemplates != null && webTemplates.size() > 0;
		
		// 1. restore site db, the priority is site db --> template db --> default db (saas)
		boolean isEclipseEnv = "true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE));
		String siteInitSql = getInitSql(siteName, isEclipseEnv);
		if (new File(siteInitSql).exists()) {
			// TODO: backup the existed db?
			DbTools.getInstance().restore(siteInitSql, siteName);
		} else if (!hasTemplate) {
//			String defaultInitSql = getInitSql(Website.DEFAULT_NAME, isEclipseEnv);
//			DbTools.getInstance().restore(defaultInitSql, siteName);
		}
		
		if (hasTemplate) {
			for (Website webTemplate : webTemplates) {
				// init template db
				String template = webTemplate.getName();
				String initSql = getInitSql(template, isEclipseEnv);
				if (!new File(initSql).exists()) {
					// back up the template db
					DbTools.getInstance().backupTableStructure(initSql, template, null);
					
					String coreDbFile = getInitSql(Website.DEFAULT_NAME, isEclipseEnv);
					try {
						String coreData = FileUtils.readFileToString(new File(coreDbFile), "utf-8");
						FileWriter fileWritter = new FileWriter(initSql, true);
		    	        BufferedWriter bufferWritter = new BufferedWriter(fileWritter);
		    	        bufferWritter.write(coreData);
		    	        bufferWritter.close();
					} catch (IOException ex) {
						logger.error("Failed to prepare the site init sql", ex);
						throw new ApplicationException("errors.system");
					}
				}
				DbTools.getInstance().restore(initSql, siteName);
				
				/* TODO: comment it?
				// 3. Template file 
				String srcTplDir = config.getRealSitePath(template);
				String destTplDir = config.getRealSitePath(siteName);
				try {
					copyDir(srcTplDir, destTplDir, new String[] {"pagesGen", "tmp"});
				} catch (Exception ex) {
					logger.error("Failed to copy the template file");
				}*/
			}
			
			// backup current site script
			DbTools.getInstance().backup(siteInitSql, siteName);
		}
		
		// copy Plugin folder
		installWebsitePlugin(siteName, website, true, true);		// TODO: use 'false'?
		
		if (logger.isDebugEnabled()) {
			logger.debug("Finish init site: " + siteName);
		}

		// update site status
		genericService.executeInSite(Website.DEFAULT_NAME, new Executor<Object, Website>() {
			@Override
			public Object execute(Website website) {
				try {
					website.setStatus(Website.STATUS_ACTIVE);
					Website dbWebsite = genericService.load(Website.class, website.getId());
					dbWebsite.setStatus(Website.STATUS_ACTIVE);
					genericService.merge(dbWebsite);
					website.setId(dbWebsite.getId());
				} catch (Exception ex) {
					logger.error("install site: " + website.getName(), ex);
				}
				return website;
			}
		}, website);
		
		// update cache
		websites.put(website.getName(), website);		// add to cache firstly, avoid the dead cycle
		
		try {
			// create the default administrator user
			checkAndCreateSuperAdmin(website);
			
			if (startupIt) {
				startup(website.getName());
			}
		} catch (Exception ex) {
			websites.remove(website.getName());
			logger.error("Failed to startup site: " + website.getName(), ex);
		}
	}

	private void checkAndCreateSuperAdmin(Website website) {
		genericService.executeInSite(website.getName(), new Executor<Object, Website>() {
			@Override
			public Object execute(Website website) {
				LoginUser user = new LoginUser();
				user.setName(LoginUser.SUPER_ADMIN);
				LoginUser dbUser = genericService.searchAll(user).getData(0);
				LoginUser admin; 
				if (dbUser == null) {
					user.setPassword(website.getPassword());
					admin = genericService.merge(user);
				} else {
					admin = dbUser;
				}

				// plugin record
				PageUpgradeService pageUpgradeService = ObjectFactory.getInstance()
						.getBean(PageUpgradeService.class, false);
				// check site folder
				String siteDir = config.getRealSitePath(website.getName());
				if (!new File(siteDir).exists()) {
					// reset the update record
					pageUpgradeService.resetUpgradeTime();
				}
				return admin;
			}
		}, website);
	}

	public synchronized void startup(String site) {
		Website website = getWebsite(site);
		
		int status = website.getStatus();
		if (!Website.ACTIVE_STATUS_STARTUP.equals(website.getStatus())) {
			if (logger.isDebugEnabled()) {
				logger.debug("Startup site: " + site);
			}
			
			checkAndCreateSuperAdmin(website);
			
			installWebsitePlugin(site, website, true, true);
			
			// check & try to install website
			/*
			if (DbTools.getInstance().dbIsReady(null)) {
				// update the db script of the new site
				genericService.executeInSite(site, new Executor<Object, Website>() {
					@Override
					public Object execute(Website website) {
						DynamicModelManager dynamicModelManager = ObjectFactory.getInstance()
								.getBean(DynamicModelManager.class, false);
						// TODO: don't refresh site if the site is DEFAULT_SITE? see method loadWebSite()
						dynamicModelManager.refreshDbSchema();
						return null;
					}
				}, website);
				
				// only check the super admin
				checkAndCreateSuperAdmin(website);
				
				installWebsitePlugin(site, website, true, true);
			} else {
				// install site & create super admin
				install(website, false);
				

				// update the db script of the new site
				genericService.executeInSite(site, new Executor<Object, Website>() {
					@Override
					public Object execute(Website website) {
						DynamicModelManager dynamicModelManager = ObjectFactory.getInstance()
								.getBean(DynamicModelManager.class, false);
						// TODO: don't refresh site if the site is DEFAULT_SITE? see method loadWebSite()
						dynamicModelManager.refreshDbSchema();
						return null;
					}
				}, website);
			}
			
			*/
			
			
			// update plugin of the new site
			genericService.executeInSite(site, new Executor<Object, Website>() {
				@Override
				public Object execute(Website website) {
					PageUpgradeService pageUpgradeService = ObjectFactory.getInstance()
							.getBean(PageUpgradeService.class, false);
					Collection<File> plugins = scanPlugin(website.getName(), new ArrayList<String>(0));
					
					// plugin
					// site plugin-in (only update current site product)
					pageUpgradeService.upgradePlugins(ognlExUtil.getXWorkConverter(), false, true, plugins);
					return null;
				}

				private Collection<File> scanPlugin(String siteName, final Collection<String> disabledPlugins) {
					String upgradeDir = config.getRealSitePath(siteName);
					File upgradeFolder = new File(upgradeDir);
					Collection<File> zipFiles = FileUtils.listFiles(upgradeFolder, new String[] {"zip"}, false);
					Collection<File> xmlFiles = FileUtils.listFiles(new File(upgradeFolder, "pages"), new String[] {"xml"}, true);
					List<File> files = new ArrayList<>(zipFiles.size() + xmlFiles.size());
					if (disabledPlugins.size() == 0) {
						files.addAll(zipFiles);
						files.addAll(xmlFiles);
					} else {
						for (File file : zipFiles) {
							String name = file.getName();
							String nameNoExtension = name.substring(0, name.length() - 4);
							if (!disabledPlugins.contains(nameNoExtension)) {
								files.add(file);
							}
						}
						String sitePrefix = new File(config.getRealSitePath(), config.getViewDir()).getPath();
						for (File file : xmlFiles) {
							String name = file.getPath();
							String nameNoExtension = name.substring(sitePrefix.length() + 1, name.length() - 4);
							String pluginName = nameNoExtension.replace(File.separatorChar, '.');
							if (!disabledPlugins.contains(pluginName)) {
								files.add(file);
							}
						}
					}
					return files;
				}
			}, website);
			
			// do the next step after the plug-in data committed
			// update the dynamic model of the new site
			genericService.executeInSite(site, new Executor<Object, Website>() {
				@Override
				public Object execute(Website website) {
					// 2. dynamic model
					DynamicModelService dynamicModelService = ObjectFactory.getInstance()
							.getBean(DynamicModelService.class, false);
					dynamicModelService.checkAndUpdateDynamicModel(website.getName());
					
					// 3. initialize the site variables
					Set<String> siteProduct = new HashSet<String>();
					siteProduct.add(Website.DEFAULT_NAME);				// all the site extends the default site
					calculateSiteProducts(siteProduct, website);
					Initialization[] initializations = objectFactory.getBeans(Initialization.class, true);
					if (initializations != null) {
						for (Initialization initialization : initializations) {
							boolean contain = false;
							for (String name : initialization.getProductName()) {
								if (siteProduct.contains(name)) {
									contain = true;
									break;
								}
							}
							if (contain) {
								initialization.initSite();
							}
						}
					}
					
					// 4. login page & free page 	TODO: here?
					if (StringUtils.isNotEmpty(website.getLoginPage())) {
						safeManager.setLoginPage(website.getLoginPage());
					}
					
					website.setStatus(Website.ACTIVE_STATUS_STARTUP);
					return null;
				}
			}, website);
			
			// update status to Website.STATUS_ACTIVE
			if (status != Website.STATUS_ACTIVE) {
				Website siteStatus = new Website();
				siteStatus.setId(website.getId());
				siteStatus.setStatus(Website.STATUS_ACTIVE);
				updateSiteStatus(siteStatus);
			}
		}
	}

	/**
	 * Install the site plug-in
	 * 
	 * @param site
	 * @param website
	 * @param onlyNewVersion copy the source file only when the source file is newer than the dest file
	 * @param onlyNewFile copy the source file only when the dest file doesn't exist
	 */
	private void installWebsitePlugin(String site, Website website,
			boolean onlyNewVersion, boolean onlyNewFile) {
		try {
			String productName = website.getProductName();
			Collection<String> disabledPlugins = getDisabledPlugins(productName);
			
			// check if the setup file has new version
			String destPluginDirPath = config.getRealSitePath(site);
			File destPluginDir = new File(destPluginDirPath);
			// core directory
			String corePluginDir = config.getPluginDir("core");
			
//			String selfPluginDir = config.getPluginDir(site);
			
			String selfPluginDir = config.getPluginDir(productName);
			
			File coreFile = new File(corePluginDir);
			File selfFile = new File(selfPluginDir);
			if (onlyNewVersion || onlyNewFile) {
				if (coreFile.exists()) {
					syncDirectory(coreFile, destPluginDir, onlyNewFile, disabledPlugins);
				}
				if (selfFile.exists()) {
					syncDirectory(selfFile, destPluginDir, onlyNewFile, disabledPlugins);
				}
			} else {
				if (coreFile.exists()) {
					copyDirectory(coreFile, destPluginDir, disabledPlugins);
				}
				if (selfFile.exists()) {
					copyDirectory(selfFile, destPluginDir, disabledPlugins);
				}
			}
			syncPluginFolder(coreFile, destPluginDir, disabledPlugins);
			syncPluginFolder(selfFile, destPluginDir, disabledPlugins);
			
			// template directory
			if (website.getWebsiteTemplates() != null) {
				for (Website webTemplate : website.getWebsiteTemplates()) {
					String templatePluginDir = config.getPluginDir(webTemplate.getName());
					File templatePluginDirFile = new File(templatePluginDir);
					if (onlyNewVersion || onlyNewFile) {
						syncDirectory(templatePluginDirFile, destPluginDir, onlyNewFile, disabledPlugins);
					} else {
						copyDirectory(templatePluginDirFile, destPluginDir, disabledPlugins);
					}
					syncPluginFolder(templatePluginDirFile, destPluginDir, disabledPlugins);
				}
			}
		} catch (IOException ex) {
			logger.error("Failed to synchronize the folder", ex);
		}
	}
	
	private void syncPluginFolder(File pluginFolder, File destFolder, Collection<String> disabledPlugins) {
		File[] files = pluginFolder.listFiles();
		if (files != null) {
			for (File file : files) {
				if (file.isDirectory()) {
					String pluginName = file.getName();
					if (disabledPlugins.contains(pluginName)) {
						continue;
					}
					
					fileSyncService.addSyncFile(file, destFolder, true);
				} else {
					// ignore the zip file
				}
			}
		}
	}
	
	private void syncDirectory(File srcDir, File destDir, boolean onlyNewFile, Collection<String> disabledPlugins) throws IOException {
		for (File file : srcDir.listFiles()) {
			boolean isDir = file.isDirectory();
			String pluginName = file.getName();
			if (!isDir) {
				int index = pluginName.lastIndexOf('.');
				if (index > 0) {
					pluginName = pluginName.substring(0, index);		// remove suffix
				}
			}
			if (disabledPlugins.contains(pluginName)) {
				continue;
			}
			
			if (isDir) {
				// /plugins/xxxPlugin/* --> /site/xxx/*
				FileUtil.syncDirectory(file, destDir, true, onlyNewFile);
			} else {
				// /plugins/*.zip --> /site/xxx/*.zip
				File destFile = new File(destDir, file.getName());
				if (!destFile.exists() || !onlyNewFile && destFile.lastModified() < file.lastModified()) {
					FileUtils.copyFile(file, destFile, true);
				}
			}
		}
	}
	
	private void copyDirectory(File srcDir, File destDir, Collection<String> disabledPlugins) throws IOException {
		for (File file : srcDir.listFiles()) {
			boolean isDir = file.isDirectory();
			String pluginName = file.getName();
			if (!isDir) {
				int index = pluginName.lastIndexOf('.');
				if (index > 0) {
					pluginName = pluginName.substring(0, index);		// remove suffix
				}
			}
			if (disabledPlugins.contains(pluginName)) {
				continue;
			}
			
			if (isDir) {
				// /plugins/xxxPlugin/* --> /site/xxx/*
				FileUtils.copyDirectory(file, destDir, true);
			} else {
				// /plugins/*.zip --> /site/xxx/*.zip
				File destFile = new File(destDir, file.getName());
				FileUtils.copyFile(file, destFile, true);
			}
		}
	}
	
	private void updateSiteStatus(Website website) {
		genericService.executeInSite(Website.DEFAULT_NAME, new Executor<Object, Website>() {
			@Override
			public Object execute(Website website) {
				try {
					Website dbWebsite = genericService.load(Website.class, website.getId());
					dbWebsite.setStatus(website.getStatus());
					genericService.merge(dbWebsite);
				} catch (Exception ex) {
					logger.error("update site status: " + website.getName(), ex);
				}
				return website;
			}
		}, website);
	}

	private void calculateSiteProducts(Set<String> siteProduct, Website website) {
		siteProduct.add(website.getProductName());			// site is also a product
		if (website.getWebsiteTemplates() != null) {
			for (Website site : website.getWebsiteTemplates()) {
				if (!siteProduct.contains(site.getProductName())) {
					calculateSiteProducts(siteProduct, site);
				}
			}
		}
	}
	
	public void stop(String site) {
		if (logger.isDebugEnabled()) {
			logger.debug("Stop site: " + site);
		}
		
		site = site.intern();
		
		// TODO: remove dynamic class
		dynamicClassLoaderFactory.unloadSiteClassLoader(site);
		
		// remove cache
//		cacheService.removeSiteCache(site);
		
		// shutdown connection
		multiSiteDataSource.shutdown(site);
		
		// change the site status to stop
		Website website = getWebsite(site);
		if (website != null) {
			website.setStatus(Website.STATUS_STOP);
		}
	}
	
	public void uninstall(String site) {
		if (logger.isDebugEnabled()) {
			logger.debug("Uninstall site: " + site);
		}
		
		stop(site);
		site = site.intern();
		
		Website website = getWebsite(site);
		if (website != null) {
			website.setStatus(Website.STATUS_DISABLE);
		}
		
		// backup resource
		// remove db
		// remove file
		
		// remove the site initialization sql, see 'install'
		String dbHome = System.getProperty(SystemEnv.DB_HOME);
		String initSql = dbHome + "backup/" + site + "-init.sql";
		File initSqlFile = new File(initSql);
		if (initSqlFile.exists()) {
			initSqlFile.delete();
		}
	}
	
	public Website _update(Website model) {
		Website website = genericService.view(model);
		model.setPassword(null);		// don't show the encrypted password
		return website;
	}
	
	public Website update(Website model) {
		checkSiteName(model.getName());
		model.setModifyOn(Calendar.getInstance().getTime());
		if (model.getPassword() != null) {		// use new password
			String password = loginUserService.getEncryptPassword(model.getPassword());
			model.setPassword(password);
		} else {
			// keep the original password
			String password = genericService.load(model).getPassword();
			model.setPassword(password);
		}
		Website website = genericService.update(model);
		
		website = website.clone();
		updateCurrentUserPrivilege(model);
		
		// update cache
		if (website.getWebsiteTemplates() != null) {
			for (Website template : website.getWebsiteTemplates()) {
				template.getName();		// force load
			}
		}
		websites.put(website.getName(), website);
		
		if (Website.STATUS_INIT.equals(website.getStatus())) {
			getWebsiteService().install(website, true);
		} if (Website.STATUS_ACTIVE.equals(website.getStatus())) {
			startup(website.getName());
		} else if (Website.STATUS_STOP.equals(website.getStatus())) {
			stop(website.getName());
		} else if (Website.STATUS_DISABLE.equals(website.getStatus())) {
			uninstall(website.getName());
		}
		return website;
	}
	
	public boolean deleteList(List<Website> results) {		
		// populate all the site names
		List<String> siteNames = new ArrayList<String>(results.size());
		for (Website website : results) {
			if (website != null) {
				Website dbWebsite = genericService.load(website);
				siteNames.add(dbWebsite.getName());
			}
		}
		
		boolean ret = genericService.deleteList((List)results);
		
		for (String siteName : siteNames) {
			uninstall(siteName);
			websites.remove(siteName);
		}
		return ret;
	}
	
	private void updateCurrentUserPrivilege(Website theWebsite) {
		// TODO: do this operation here?
		/*String userId = userService.getLoginUser().getId().toString();
		if (theWebsite.getHomepage() != null) {
			safeManager.addPagePermission(userId, theWebsite.getHomepage());
		} else {
			safeManager.addPagePermission(userId, "/" + theWebsite.getName() + "/index.html");
		}*/
	}
	
	// TODO: use a new service to manage the site behavior
	// 1. start & stop the site automatically (use site session?)
	// 2. send message to notify use to rent
	public void manageSite() {
		
	}
	
	public void checkAndInitSite() {
		// scan plug-in folder and add the site
		List<String> siteNames = new ArrayList<String>();
		// TODO: scan plug-in folder
		
		// TODO: performance issue?
		for (String siteName : siteNames) {
			Website website = new Website();
			website.setCompId(Integer.parseInt(siteName));
//			website.setName(siteName);
			Website dbWebsite = genericService.searchAll(website).getData(0);
			if (dbWebsite == null) {
				website.setStatus(Website.STATUS_INIT);
				genericService.merge(website);
			}
		}
	}
	
	public void setMaxSiteCount(int maxSiteCount) {
		this.maxSiteCount = maxSiteCount;
	}
	
	
	private Collection<String> getDisabledPlugins(String siteName) {
		Collection<String> disabledPlugins = new HashSet<String>();
		Properties properties = getSiteProperties(siteName);
		for (Object key : properties.keySet()) {
			if (key instanceof String) {
				String sKey = (String)key;
				if (sKey.startsWith("plugins.")) {
					String value = properties.getProperty(sKey).trim();
					if ("false".equals(value)) {
						String pluginName = sKey.substring(8).trim();
						disabledPlugins.add(pluginName);
					}
				}
			}
		}
		return disabledPlugins;
	}
	
	private Properties getSiteProperties(String site) {
		Properties properties = new Properties();
		String siteCfg = config.getPluginDir(site) + "website.properties";
		File siteCfgFile = new File(siteCfg);
		if (siteCfgFile.exists()) {
			try {
				properties.load(new FileInputStream(siteCfgFile));
			} catch (IOException ex) {
				logger.error("Failed to load site properties", ex);
			}
		}
		return properties;
	}
}